diff options
Diffstat (limited to 'app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx new file mode 100644 index 00000000..c3136496 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx @@ -0,0 +1,202 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContractsByTemplateId, getBasicContractTemplateInfo } from "@/lib/basic-contract/service" +import { searchParamsCacheByTemplateId } from "@/lib/basic-contract/validations" +import { InformationButton } from "@/components/information/information-button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { FileText, Calendar, AlertTriangle, ArrowLeft } from "lucide-react" +import { formatDateTime } from "@/lib/utils" +import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from "@/components/ui/breadcrumb" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { BasicContractsDetailTable } from "@/lib/basic-contract/status-detail/basic-contracts-detail-table" + +interface IndexPageProps { + params: { + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + const templateId = parseInt(id, 10) + + if (isNaN(templateId)) { + return ( + <Shell> + <div className="text-center py-10"> + <h1 className="text-2xl font-bold text-gray-900">잘못된 템플릿 ID</h1> + <p className="text-gray-500 mt-2">올바른 템플릿을 선택해주세요.</p> + </div> + </Shell> + ) + } + + const searchParams = await props.searchParams + const search = searchParamsCacheByTemplateId.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 템플릿 정보 조회 + const templateInfoPromise = getBasicContractTemplateInfo(templateId) + + // 계약서 목록 조회 + const contractsPromise = getBasicContractsByTemplateId( + { + ...search, + filters: validFilters, + }, + templateId + ) + + const promises = Promise.all([contractsPromise]) + + return ( + <Shell className="gap-2"> + {/* 상단 헤더: Breadcrumb (좌) + 뒤로가기 버튼(우) */} + <div className="flex items-center justify-between"> + <Breadcrumb> + <BreadcrumbList> + <BreadcrumbItem> + <BreadcrumbLink href="/evcp">EVCP</BreadcrumbLink> + </BreadcrumbItem> + <BreadcrumbSeparator /> + <BreadcrumbItem> + <BreadcrumbLink href="/evcp/basic-contract">기본계약서/서약서 관리</BreadcrumbLink> + </BreadcrumbItem> + <BreadcrumbSeparator /> + <BreadcrumbItem> + <BreadcrumbPage>기본계약서/서약서 관리 상세</BreadcrumbPage> + </BreadcrumbItem> + </BreadcrumbList> + </Breadcrumb> + + <Button asChild variant="outline" size="sm"> + <Link href="/evcp/basic-contract"> + <ArrowLeft className="h-4 w-4 mr-2" /> + 목록으로 돌아가기 + </Link> + </Button> + </div> + + {/* 템플릿 정보 섹션 (콤팩트) */} + <React.Suspense fallback={<TemplateInfoSkeleton />}> + <TemplateInfo templateInfoPromise={templateInfoPromise} /> + </React.Suspense> + + <Separator /> + + {/* 계약서 리스트 제목 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + <h2 className="text-xl font-semibold">계약서 목록</h2> + <InformationButton pagePath="partners/basic-contract-detail" /> + </div> + </div> + + {/* 계약서 테이블 */} + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={2} + filterableColumnCount={3} + cellWidths={["3rem", "10rem", "15rem", "8rem", "8rem", "10rem", "10rem", "3rem"]} + shrinkZero + /> + } + > + <BasicContractsDetailTable promises={promises} templateId={templateId} /> + </React.Suspense> + </Shell> + ) +} + +// 템플릿 정보 컴포넌트 (콤팩트 버전) +async function TemplateInfo({ + templateInfoPromise, +}: { + templateInfoPromise: Promise<Awaited<ReturnType<typeof getBasicContractTemplateInfo>>> +}) { + const templateInfo = await templateInfoPromise + + if (!templateInfo) { + return ( + <Card> + <CardContent className="py-6"> + <div className="text-center text-gray-500"> + <AlertTriangle className="h-8 w-8 mx-auto mb-2" /> + <p>템플릿 정보를 찾을 수 없습니다.</p> + </div> + </CardContent> + </Card> + ) + } + + return ( + <Card> + <CardHeader className="py-4"> + <div className="flex flex-wrap items-center justify-between gap-2"> + {/* 좌측: 제목 + 리비전 */} + <div className="flex items-center gap-2 min-w-0"> + <CardTitle className="text-lg font-semibold leading-none truncate"> + {templateInfo.templateName} + </CardTitle> + <Badge variant="outline" className="shrink-0">v{templateInfo.revision}</Badge> + </div> + + {/* 우측: 상태/법무검토 */} + <div className="flex items-center gap-2"> + {templateInfo.legalReviewRequired && ( + <Badge variant="secondary" className="shrink-0">법무검토 필요</Badge> + )} + <Badge + variant={templateInfo.status === "ACTIVE" ? "default" : "secondary"} + className="shrink-0" + > + {templateInfo.status === "ACTIVE" ? "활성" : "비활성"} + </Badge> + </div> + </div> + + {/* 메타 정보 한 줄 정리 */} + <div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> + <span className="inline-flex items-center gap-1"> + <Calendar className="h-3.5 w-3.5" /> + 생성일: {formatDateTime(templateInfo.createdAt, "KR")} + </span> + {templateInfo.fileName && ( + <> + <span className="select-none">•</span> + <span className="inline-flex items-center gap-1 min-w-0"> + <FileText className="h-3.5 w-3.5" /> + <span className="truncate max-w-[52ch]">템플릿 파일: {templateInfo.fileName}</span> + </span> + </> + )} + </div> + </CardHeader> + </Card> + ) +} + +// 로딩 스켈레톤 (콤팩트) +function TemplateInfoSkeleton() { + return ( + <Card> + <CardHeader className="py-4"> + <div className="space-y-2"> + <div className="h-5 bg-gray-200 rounded w-1/2 animate-pulse" /> + <div className="h-3 bg-gray-200 rounded w-1/3 animate-pulse" /> + </div> + </CardHeader> + </Card> + ) +} |
